package rexsee.core.transportation;

import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;

import rexsee.core.application.RexseeApplication;
import rexsee.core.browser.Browser;
import rexsee.core.browser.RexseeUrl;
import rexsee.core.browser.clazz.LogListeners.LogListener;
import rexsee.core.lang.RexseeLanguage;
import rexsee.core.style.RexseeDrawable;
import rexsee.core.utilities.Escape;
import rexsee.core.utilities.Utilities;
import rexsee.multimedia.RexseeImage;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.net.Uri;
import android.webkit.CookieManager;

public class PageSaver {

	public static final String KEY_SAVE_STATUS = "status";
	public static final String KEY_SAVE_TITLE = "title";
	public static final String KEY_SAVE_URL = "url";
	public static final String STATUS_SAVE_ONGOING = "ongoing";
	public static final String STATUS_SAVE_SUCCEED = "succeed";
	public static final String STATUS_SAVE_FAILED = "failed";
	public static final String DATE_SAVE = "date";

	private final Context mContext;
	private final PageSaveInterface mPageSave;

	private String mTitle;
	private String mUrl;
	private String mRoot;

	public PageSaver(Context context, PageSaveInterface pageSave) {
		mContext = context;
		mPageSave = pageSave;
	}

	public void start(String title, String url, final Runnable finishRunnable, boolean newThread) {
		mTitle = title;
		mUrl = url;
		if (mUrl == null || !mUrl.toLowerCase().startsWith("http://")) {
			log("download", "Only http address can be downloaded.");
			return;
		}
		mPageSave.remove(mUrl);
		final String cfg = mPageSave.getConfigPath(mUrl);
		mRoot = cfg.substring(0, cfg.lastIndexOf("/") + 1);
		String config = KEY_SAVE_STATUS + "=" + STATUS_SAVE_ONGOING + ";" + KEY_SAVE_TITLE + "=" + Escape.escape(mTitle) + ";" + KEY_SAVE_URL + "=" + Escape.escape(mUrl) + ";" + DATE_SAVE + "=" + Utilities.getDateAndTime();
		if (!Utilities.putContent(cfg, config.getBytes())) {
			log("download", "can not create config file.");
			return;
		}
		Runnable runnable = new Runnable() {
			@Override
			public void run() {
				boolean rtn;
				if (isImage(mUrl) || isText(mUrl)) {
					rtn = downloadImage(mUrl, mPageSave.getNonHtmlPath(mUrl));
				} else if (isDownloadable(mUrl)) {
					rtn = downloadDownloadable(mUrl, mPageSave.getNonHtmlPath(mUrl));
				} else {
					rtn = downloadPage();
				}
				String config;
				if (rtn) {
					config = KEY_SAVE_STATUS + "=" + STATUS_SAVE_SUCCEED + ";" + KEY_SAVE_TITLE + "=" + Escape.escape(mTitle) + ";" + KEY_SAVE_URL + "=" + Escape.escape(mUrl) + ";" + DATE_SAVE + "=" + Utilities.getDateAndTime();
				} else {
					config = KEY_SAVE_STATUS + "=" + STATUS_SAVE_FAILED + ";" + KEY_SAVE_TITLE + "=" + Escape.escape(mTitle) + ";" + KEY_SAVE_URL + "=" + Escape.escape(mUrl) + ";" + DATE_SAVE + "=" + Utilities.getDateAndTime();
				}
				if (!Utilities.putContent(cfg, config.getBytes())) {
					log("download", "can not update config file.");
				}
				if (finishRunnable != null) finishRunnable.run();
			}
		};
		if (newThread) {
			new Thread(runnable).start();
		} else {
			runnable.run();
		}
	}

	private void log(String func, String message) {
		mPageSave.log(mUrl, this.getClass().getName() + "." + func, message);
	}

	private boolean downloadPage() {

		String source = Utilities.getContent(mContext, mUrl, new LogListener() {
			@Override
			public void run(Context context, Browser browser, int type, String message) {
				log("downloadPage", message);
			}
		});
		if (source == null) {
			log("downloadPage", "null source.");
			return false;
		}
		String filesDir = mPageSave.getFilesDir(mUrl);
		File file = new File(Uri.parse(filesDir).getPath());
		if (file.exists() && !file.delete()) {
			log("downloadPage", "can not delete existing files dir.");
			return false;
		}
		if (!file.mkdirs()) {
			log("downloadPage", "can not make new files dir.");
			return false;
		}

		source = processTagLink(source);
		source = processTagScript(source);
		source = processTagImg(source);
		source = processTagEmbed(source);
		source = processTagA(source);
		source = processTagForm(source);
		source = processStyle(source, null);

		String path = mPageSave.getHtmlPath(mUrl);
		String charset = Utilities.guessHtmlCharset(source);
		byte[] bytes;
		if (charset == null) {
			bytes = source.getBytes();
		} else {
			try {
				bytes = source.getBytes(charset);
			} catch (UnsupportedEncodingException e) {
				bytes = source.getBytes();
			}
		}
		if (!Utilities.putContent(path, bytes)) {
			log("downloadPage", "can not put content to html file.");
			return false;
		} else {
			return true;
		}

	}

	private String processTagLink(String source) {
		ArrayList<String> list = getAttributes(source, "<link ", ">", "href=");
		for (int i = 0; i < list.size(); i++) {
			if (isNull(list.get(i))) continue;
			String link = getAbsolute(list.get(i));
			String path = getCleanedPath(link);
			downloadCss(link, path);
			source = source.replace(list.get(i), getRelativePath(path));
		}
		return source;
	}
	private String processTagScript(String source) {
		ArrayList<String> list = getAttributes(source, "<script ", ">", "src=");
		for (int i = 0; i < list.size(); i++) {
			if (isNull(list.get(i))) continue;
			String link = getAbsolute(list.get(i));
			String path = getCleanedPath(link);
			downloadScript(link, path);
			source = source.replace(list.get(i), getRelativePath(path));
		}
		return source;
	}
	private String processTagImg(String source) {
		ArrayList<String> list = getAttributes(source, "<img ", ">", "src=");
		for (int i = 0; i < list.size(); i++) {
			if (isNull(list.get(i))) continue;
			String link = getAbsolute(list.get(i));
			String path = getCleanedPath(link);
			downloadImage(link, path);
			source = source.replace(list.get(i), getRelativePath(path));
		}
		return source;
	}
	private String processTagEmbed(String source) {
		ArrayList<String> list = getAttributes(source, "<embed ", ">", "src=");
		for (int i = 0; i < list.size(); i++) {
			if (isNull(list.get(i))) continue;
			String link = getAbsolute(list.get(i));
			String path = getCleanedPath(link);
			downloadDownloadable(link, path);
			source = source.replace(list.get(i), getRelativePath(path));
		}
		return source;
	}
	private String processStyle(String source, String cssPath) {
		ArrayList<String> clauses;
		if (cssPath != null) {
			clauses = new ArrayList<String>();
			clauses.add(source);
		} else {
			clauses = getTagString(source, "<style>", "</style>");
		}
		String attributeName = "url(";
		ArrayList<String> list = new ArrayList<String>();
		for (int i = 0; i < clauses.size(); i++) {
			String clause = clauses.get(i);
			clause = clause.replace(attributeName.toUpperCase(), attributeName);
			while (clause.contains(attributeName)) {
				String attribute = getAttribute(attributeName, clause);
				if (attribute != null && !list.contains(attribute)) list.add(attribute);
				clause = clause.substring(clause.indexOf(attributeName) + attributeName.length());
			}
		}
		for (int i = 0; i < list.size(); i++) {
			if (isNull(list.get(i))) continue;
			String link = (cssPath == null) ? getAbsolute(list.get(i)) : getAbsolute(list.get(i), cssPath);
			String path = getCleanedPath(link);
			//log("processStyle", "found image in css: " + list.get(i) + "=>" + link);
			downloadImage(link, path);
			source = source.replace(list.get(i), getRelativePath(path));
		}
		return source;
	}
	private String processTagA(String source) {
		ArrayList<String> list = getAttributes(source, "<a ", ">", "href=");
		for (int i = 0; i < list.size(); i++) {
			if (isNull(list.get(i)) || isAbsolute(list.get(i))) continue;
			source = source.replace(list.get(i), getAbsolute(list.get(i)));
		}
		return source;
	}
	private String processTagForm(String source) {
		ArrayList<String> list = getAttributes(source, "<form ", ">", "action=");
		for (int i = 0; i < list.size(); i++) {
			if (isNull(list.get(i)) || isAbsolute(list.get(i))) continue;
			source = source.replace(list.get(i), getAbsolute(list.get(i)));
		}
		return source;
	}

	private String getRelativePath(String cleanedPath) {
		if (cleanedPath.startsWith(mRoot)) cleanedPath = cleanedPath.replace(mRoot, "");
		return cleanedPath;
	}
	private String getCleanedPath(String link) {
		String ext = getCleanedUrl(link);
		ext = (ext.contains(".")) ? ext.substring(ext.lastIndexOf(".") + 1) : null;
		String filename = Utilities.md5(link, true);
		if (ext != null) filename = filename + "." + ext;
		String path = getCleanedUrl(mPageSave.getFilesDir(mUrl) + "/" + filename);
		return path;
	}
	private boolean isNull(String url) {
		return url == null || url.trim().equals("#") || url.trim().equals("");
	}
	private boolean isAbsolute(String url) {
		url = url.trim().toLowerCase();
		return url.startsWith("http://") || url.startsWith("https://");
	}
	private String getAbsolute(String url) {
		return (isAbsolute(url)) ? url : RexseeUrl.getAbsoluteUrl(mUrl, url);
	}
	private String getAbsolute(String url, String base) {
		return (isAbsolute(url)) ? url : RexseeUrl.getAbsoluteUrl(base, url);
	}

	public static String getCleanedUrl(String url) {
		if (url.contains("?")) url = url.substring(0, url.indexOf("?"));
		if (url.contains("&")) url = url.substring(0, url.indexOf("&"));
		if (url.contains("#")) url = url.substring(0, url.indexOf("#"));
		return url;
	}
	public static boolean isImage(String url) {
		url = getCleanedUrl(url.toLowerCase());
		return url.endsWith(".jpg") || url.endsWith(".jpeg") || url.endsWith(".png");

	}
	public static boolean isText(String url) {
		url = getCleanedUrl(url.toLowerCase());
		return url.endsWith(".txt")
				|| url.endsWith(".xml")
				|| url.endsWith(".js")
				|| url.endsWith(".css");
	}
	public static boolean isDownloadable(String url) {
		url = getCleanedUrl(url.toLowerCase());
		return url.endsWith(".mp3")
				|| url.endsWith(".mp4")
				|| url.endsWith(".mpeg")
				|| url.endsWith(".mpeg4")
				|| url.endsWith(".3gp")
				|| url.endsWith(".3ga")
				|| url.endsWith(".3gpp")
				|| url.endsWith(".apk")
				|| url.endsWith(".wmv")
				|| url.endsWith(".wma")
				|| url.endsWith(".amr")
				|| url.endsWith(".avi")
				|| url.endsWith(".rm")
				|| url.endsWith(".rtf")
				|| url.endsWith(".doc")
				|| url.endsWith(".docx")
				|| url.endsWith(".ppt")
				|| url.endsWith(".ppts")
				|| url.endsWith(".pptx")
				|| url.endsWith(".xls")
				|| url.endsWith(".xlsx")
				|| url.endsWith(".psd")
				|| url.endsWith(".zip")
				|| url.endsWith(".rar")
				|| url.endsWith(".swf")
				|| url.endsWith(".flv")
				|| url.endsWith(".jar")
				|| url.endsWith(".pdf");

	}

	public boolean downloadDownloadable(String url, String path) {
		int timeout = 30000;
		try {
			URL sourceUrl = new URL(url);
			File targetFile = Utilities.prepareWriteFile(path);
			if (targetFile == null) {
				log("downloadDownloadable", "target file error.");
				return false;
			}
			HttpURLConnection urlConnection = (HttpURLConnection) sourceUrl.openConnection();
			CookieManager cookieManager;
			try {
				cookieManager = CookieManager.getInstance();
				String cookie = cookieManager.getCookie(url);
				if (cookie != null) urlConnection.setRequestProperty("Cookie", cookie);
			} catch (Exception e) {
				cookieManager = null;
			}
			urlConnection.setRequestProperty("User-Agent", RexseeApplication.getUserAgent(mContext));
			urlConnection.setRequestMethod("GET");
			//urlConnection.setDoOutput(true);
			urlConnection.setConnectTimeout(timeout);
			urlConnection.setReadTimeout(timeout);
			urlConnection.connect();
			if (cookieManager != null && cookieManager.acceptCookie()) {
				String cookieVal = null;
				String cookieKey = null;
				String sessionCookie = "";
				for (int i = 1; (cookieKey = urlConnection.getHeaderFieldKey(i)) != null; i++) {
					if (cookieKey.equalsIgnoreCase("set-cookie")) {
						cookieVal = urlConnection.getHeaderField(i);
						cookieVal = cookieVal.substring(0, cookieVal.indexOf(";"));
						sessionCookie += cookieVal + ";";
					}
				}
				if (sessionCookie != "") cookieManager.setCookie(url, cookieVal);
			}
			FileOutputStream fileOutput = new FileOutputStream(targetFile);
			InputStream inputStream = urlConnection.getInputStream();
			//int totalSize = urlConnection.getContentLength();
			//int downloadedSize = 0;
			byte[] buffer = new byte[1024];
			int bufferLength = 0;
			while ((bufferLength = inputStream.read(buffer)) > 0) {
				fileOutput.write(buffer, 0, bufferLength);
				//downloadedSize += bufferLength;
			}
			inputStream.close();
			fileOutput.close();
			urlConnection.disconnect();
			return true;
		} catch (Exception e) {
			log("downloadDownloadable", e.getLocalizedMessage());
			return false;
		}
	}
	private boolean downloadImage(String url, String path) {
		try {
			if (isImage(path)) {
				BitmapDrawable drawable = (BitmapDrawable) RexseeDrawable.getDrawable(mContext, null, url, null);
				if (drawable != null) {
					Bitmap bitmap = drawable.getBitmap();
					File tFile = Utilities.prepareWriteFile(path);
					try {
						FileOutputStream fileOutput = new FileOutputStream(tFile);
						bitmap.compress(RexseeImage.getFormat(path), 90, fileOutput);
						fileOutput.flush();
						fileOutput.close();
						return true;
					} catch (Exception e) {
						log("downloadImage", e.getLocalizedMessage() + "<" + url + ">");
						return false;
					}
				} else {
					log("downloadImage", "null bitmap<" + url + ">.");
					return false;
				}
			} else {
				byte[] bytes = Utilities.getBytes(mContext, url, new LogListener() {
					@Override
					public void run(Context context, Browser browser, int type, String message) {
						log("downloadImage", message);
					}
				});
				if (bytes != null && Utilities.putContent(path, bytes)) {
					return true;
				} else {
					log("downloadImage", "null bytes or put bytes error.");
					return false;
				}
			}
		} catch (Exception e) {
			log("downloadImage", e.getLocalizedMessage());
			return false;
		}
	}
	private boolean downloadScript(String url, String path) {
		try {
			String content = Utilities.getContent(mContext, url, new LogListener() {
				@Override
				public void run(Context context, Browser browser, int type, String message) {
					log("downloadScript", message);
				}
			});
			if (content != null && Utilities.putContent(path, content.getBytes())) {
				return true;
			} else {
				log("downloadScript", "null content or put content error.");
				return false;
			}
		} catch (Exception e) {
			log("downloadScript", e.getLocalizedMessage());
			return false;
		}
	}
	private boolean downloadCss(String url, String path) {
		try {
			String content = Utilities.getContent(mContext, url, new LogListener() {
				@Override
				public void run(Context context, Browser browser, int type, String message) {
					log("downloadCss", message);
				}
			});
			if (content == null) {
				log("downloadCss", "null content.");
				return false;
			}
			content = processStyle(content, url);
			if (Utilities.putContent(path, content.getBytes())) {
				return true;
			} else {
				log("downloadCss", "put content error.");
				return false;
			}
		} catch (Exception e) {
			log("downloadCss", e.getLocalizedMessage());
			return false;
		}
	}

	private ArrayList<String> getTagString(String source, String tagName, String tagEnd) {
		ArrayList<String> rtn = new ArrayList<String>();
		if (tagName == null) {
			log("getTagString", "null tag name.");
			return rtn;
		}
		if (source == null) {
			log("getTagString", "[" + tagName + "]null source.");
			return rtn;
		}
		try {
			String[] tmp = source.split(tagName.toLowerCase());
			if (tmp != null && tmp.length > 1) {
				for (int i = 1; i < tmp.length; i++) {
					String clause = tmp[i].substring(0, tmp[i].indexOf(tagEnd));
					if (clause != null && !clause.trim().equals("")) {
						rtn.add(clause);
					}
				}
			}
			tmp = source.split(tagName.toUpperCase());
			if (tmp != null && tmp.length > 1) {
				for (int i = 1; i < tmp.length; i++) {
					String clause = tmp[i].substring(0, tmp[i].indexOf(tagEnd));
					if (clause != null && !clause.trim().equals("")) {
						rtn.add(clause);
					}
				}
			}
			return rtn;
		} catch (Exception e) {
			log("getTagString", "[" + tagName + "]" + e.getLocalizedMessage());
			return rtn;
		}
	}
	private ArrayList<String> getAttributes(String source, String tagName, String tagEnd, String attributeName) {
		ArrayList<String> rtn = new ArrayList<String>();
		ArrayList<String> clauses = getTagString(source, tagName, tagEnd);
		if (clauses == null || clauses.size() == 0) return rtn;
		for (int i = 0; i < clauses.size(); i++) {
			String clause = clauses.get(i);
			if (clause == null || clause.trim().equals("")) continue;
			String attribute = getAttribute(attributeName, clause);
			if (attribute != null && !rtn.contains(attribute)) rtn.add(attribute);
		}
		return rtn;
	}
	private String getAttribute(String attributeName, String clause) {
		if (attributeName == null) return null;
		try {
			if (clause.contains(attributeName.toLowerCase())) {
				String la = attributeName.toLowerCase();
				if (la.contains("(")) la = la.replace("(", "\\(");
				clause = clause.split(la)[1];
			} else if (clause.contains(attributeName.toUpperCase())) {
				String ua = attributeName.toUpperCase();
				if (ua.contains("(")) ua = ua.replace("(", "\\(");
				clause = clause.split(ua)[1];
			} else {
				log("getAttribute", "[" + attributeName + "]attribute not found.");
				return null;
			}
			while (clause.startsWith("\"") || clause.startsWith("'") || clause.startsWith(" "))
				clause = clause.substring(1);
			int MAX = 100000;
			int index1 = clause.indexOf(" ");
			if (index1 <= 0) index1 = MAX;
			int index2 = clause.indexOf("'");
			if (index2 <= 0) index2 = MAX;
			int index3 = clause.indexOf("\"");
			if (index3 <= 0) index3 = MAX;
			int index4 = clause.indexOf(")");
			if (index4 <= 0) index4 = MAX;
			int index = Math.min(Math.min(index1, index2), Math.min(index3, index4));
			if (index == MAX) {
				log("getAttribute", "[" + attributeName + "]attribute not found.");
				return null;
			} else {
				return clause.substring(0, index);
			}
		} catch (Exception e) {
			log("getAttribute", "[" + attributeName + "]" + e.getLocalizedMessage());
			return null;
		}
	}

	public static interface PageSaveInterface {
		public String getConfigPath(String url);
		public String getHtmlPath(String url);
		public String getNonHtmlPath(String url);
		public String getFilesDir(String url);
		public void remove(String url);
		public void log(String url, String func, String message);
	}

	public static String download(final Browser browser, String dir, boolean cacheable, String src) {
		return download(browser, dir, cacheable, src, true);
	}
	public static String download(final Browser browser, String dir, boolean cacheable, String src, boolean showProgress) {

		if (src == null || src.trim().equals("")) return "";
		if (!src.toLowerCase().startsWith("http://") && !src.toLowerCase().startsWith("https://")) return src;

		String name = Uri.parse(PageSaver.getCleanedUrl(src)).getPath();
		name = name.substring(name.lastIndexOf("/") + 1);
		String ext = (name.contains(".")) ? name.substring(name.indexOf(".")) : "";
		String path = dir + "/" + Utilities.md5(src, true) + ext;

		File file = new File(Uri.parse(path).getPath());
		if (file.exists()) {
			if (cacheable) {
				return path;
			} else {
				file.delete();
			}
		}

		PageSaver pageSaver = new PageSaver(browser.getContext(), new PageSaveInterface() {
			@Override
			public String getConfigPath(String url) {
				return null;
			}
			@Override
			public String getHtmlPath(String url) {
				return null;
			}
			@Override
			public String getNonHtmlPath(String url) {
				return null;
			}
			@Override
			public String getFilesDir(String url) {
				return null;
			}
			@Override
			public void remove(String url) {
			}
			@Override
			public void log(String url, String func, String message) {
				browser.exception("PageSaver.download", url + ":" + func + ":" + message);
			}

		});
		if (showProgress) browser.progressDialog.show(RexseeLanguage.PROGRESS_ONGOING + name);
		if (pageSaver.downloadDownloadable(src, path)) {
			return path;
		} else {
			return null;
		}

	}

}
